from datetime import datetime, date, timedelta
from random import choice

from django.db import models
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.conf import settings
from django.core.mail import send_mail
from django.utils.functional import curry
from django.conf import settings

from accounts.managers import *
from accounts.threads import *

# AccountUser has a 1:1 relationship with auth.User.
# To keep auth.User.username unqiue, prepend Account.label
# to AccountUser.username, seperated by the delimiter:
# Must be two charaters, leaving 28 characters for Account.number(20) and AccountUser.number(8).
ACCOUNT_USERNAME_DELIMITER = getattr(settings, 'ACCOUNT_USERNAME_DELIMITER', "__")


g = lambda n=5, chars='1234567890': (''.join([choice(chars) for i in range(n)]))

def new_referrer_code():
    "Generates a referrer code that can be used to create an affiliate link."
    while True:
        code = g(n=32, chars='abcdefghijklmnopqrstuvwxyz1234567890')
        try:
            Account.objects.get(referrer_code__iexact=code)
        except Account.DoesNotExist:
            return code

def new_account_number():
    "Generates a unique account number in the format {4}-{4}-{4}."
    while True:
        num = "%s-%s-%s" % (g(4), g(4), g(4))
        try:
            Account.objects.get(number=num)
        except Account.DoesNotExist:
            return num
            
def new_user_number():
    "Generates a unique 8-digit account user number."
    while True:
        num = g(8)
        try:
            AccountUser.objects.get(number=num)
        except AccountUser.DoesNotExist:
            return num


def get_account_profile_model(fail_silently=True):
    """
    Imports and returns the Model class specified by setting.ACCOUNT_PROFILE_MODULE.
    """
    from django.core.exceptions import ImproperlyConfigured
    if not getattr(settings, 'ACCOUNT_PROFILE_MODULE', False):
        if fail_silently:
            return None
        raise AccountProfileNotAvailable
    try:
        app_label, model_name = settings.ACCOUNT_PROFILE_MODULE.split('.')
        return models.get_model(app_label, model_name)
    except (ImportError, ImproperlyConfigured):
        if fail_silently:
            return None
        raise AccountProfileNotAvailable
    
    
def get_account_user_profile_model(fail_silently=True):
    from django.core.exceptions import ImproperlyConfigured
    if not getattr(settings, 'ACCOUNT_USER_PROFILE_MODULE', False):
        if fail_silently:
            return None
        raise AccountUserProfileNotAvailable
    try:
        app_label, model_name = settings.ACCOUNT_USER_PROFILE_MODULE.split('.')
        return models.get_model(app_label, model_name)
    except (ImportError, ImproperlyConfigured):
        if fail_silently:
            return None
        raise AccountUserProfileNotAvailable


class AccountProfileNotAvailable(Exception):
    """
    Raised when an account profile is not available.
    ie: `account.get_profile()` is called when no profile instance exists.
    """
    pass

    
class AccountUserProfileNotAvailable(Exception):
    """
    Raised when an account user profile is not available.
    ie: `account_user.get_profile()` is called when no user profile instance exists.
    """
    pass    


class Account(models.Model):
    
    ACTIVE = 'active'
    DISABLED = 'disabled'
    CANCELLED = 'cancelled'
    
    STATUS_CHOICES = (
        (ACTIVE, "Active"),
        (DISABLED, "Disabled"),
        (CANCELLED, "Cancelled"),
    )
    
    objects = AccountManager()
    number = models.CharField("Account #", max_length=20, unique=True)
    label = models.CharField(max_length=100, unique=True)
    site = models.ForeignKey(Site, unique=True)
    auth_owner = models.ForeignKey(User, unique=True)
    created = models.DateTimeField(default=datetime.now)
    # active = models.BooleanField(u'Enabled', default=True)
    how_heard = models.CharField(max_length=100, choices=settings.ACCOUNT_ADVERTISING_CHOICES, blank=True)
    status = models.CharField(max_length=100, choices=STATUS_CHOICES)
    next_renewal = models.DateField(null=True, blank=True)
    referrer = models.ForeignKey("accounts.Account", verbose_name="Referred by", null=True, blank=True)
    referrer_code = models.CharField(max_length=32, unique=True)
    price_override = models.DecimalField(max_digits=11, decimal_places=2, null=True, blank=True)
    
    def get_profile(self):
        """
        Returns this Account's profile.
        This allows applications to "extend" the Account with whatever application-specific fields are needed (ex: API key).
        This will fail LOUDLY if called:
            - When `ACCOUNT_PROFILE_MODULE` has NOT been defined in settings.py
            - When no model can be found (ie: MyAccountModel.DoesNotExist will be raised).
        """
        if not hasattr(self, '_profile_cache'):
            model = get_account_profile_model(fail_silently=False)
            self._profile_cache = model._default_manager.get(account__id__exact=self.id)
            self._profile_cache.accountuser = self
        return self._profile_cache
    
    def is_active(self):
        return self.status == Account.ACTIVE
        
    def is_disabled(self):
        return self.status == Account.DISABLED
        
    def is_cancelled(self):
        return self.status == Account.CANCELLED
    
    @property
    def name(self):
        return self.site.name
        
    @property
    def owner(self):
        return AccountUser.objects.get(account=self, user=self.auth_owner)
        
    @property
    def domain(self):
        return self.site.domain
        
    @property
    def plan(self):
        return self.subscriptions.all()[0].plan
        
    @property
    def subscription(self):
        return self.subscriptions.latest()
        
    def calculate_monthly_charge(self):
        """
        This function applies all the business logic necessary to calculate
        what this Account show be billed this month, taking into consideration:
        - rate plan monthly fee
        TODO:
        - absolute price override
        - any applied discounts or promo codes
        """
        if self.price_override is not None:
            return self.price_override
        return self.subscription.plan.monthly_fee
        
        
    @property
    def revenue(self):
        if self.total_revenue is None:
            return '$0'
        return "$%s" % self.total_revenue
        
    @property
    def consecutive_declines(self):
        return self._consecutive_declines
    
    def qualifies_for(self, plan):
        checks = [ test_feature(self, feature.feature.label, plan, qualify=True) for feature in plan.rateplanfeature_set.all() ]
        return all(checks)
    
    def change_to_plan(self, new_plan):
        original_plan = self.subscription.plan
        self.subscriptions.create(plan=new_plan)
        from accounts.signals import rate_plan_changed
        rate_plan_changed.send(sender=self, old_plan=original_plan, new_plan=new_plan)
    
    @property
    def billing_info(self):
        try:
            return BillingProfile.objects.get(account=self)
        except BillingProfile.DoesNotExist:
            return None
    
    def has_billing_info(self):
        return self.billing_info is not None
        
    def needs_billing_info(self):
        # Accounts with a 0.00 price override should never be asked for billing info.
        if self.price_override is not None and self.price_override <= 0:
            return False
        return (self.subscription.plan.monthly_fee != 0) and not self.has_billing_info()
    
    def is_in_trial(self):
        return not self.trial_expired() #self.days_active <= settings.TRIAL_PERIOD
    
    def trial_expired(self):
        if self.price_override is not None and self.price_override <= 0:
            return False
        return self.trial_expiry_date() <= date.today()
    
    def trial_expiry_date(self):
        """
        This is the first date that service will be disabled if no payment info exists.
        I.e, the last day of service is trial_expiry_date - 1 day
        """
        if self.price_override is not None and self.price_override <= 0:
            return date.today() + timedelta(days=365)
        return self.created.date() + timedelta(days=settings.TRIAL_PERIOD)
    
    @property
    def trial_days_left(self):
        return (self.trial_expiry_date() - date.today()).days
    
    def __unicode__(self):
        return self.name
        
    def save(self, *args, **kwargs):
        if not self.number:
            self.number = new_account_number()
        super(Account, self).save(*args, **kwargs)
        
    def delete(self, *args, **kwargs):
        self.site.delete()
        super(Account, self).delete()


class BillingProfile(models.Model):
    account = models.ForeignKey(Account, unique=True)
    created = models.DateTimeField(default=datetime.now, editable=False)
    modified = models.DateTimeField(editable=False)
    billing_token = models.CharField(max_length=100)    
    card_holder = models.CharField(max_length=100)
    card_type = models.CharField(max_length=50)
    card_end_digits = models.IntegerField(max_length=4)
    expiry = models.DateTimeField()
    
    @property
    def card_number(self):
        return u'*%s' % self.card_end_digits
        
    @property
    def expiry_date(self):
        return u'%s/%s' % (self.expiry.month, self.expiry.year)
    
    def save(self, *args, **kwargs):
        self.modified = datetime.now()
        super(BillingProfile, self).save(*args, **kwargs)
        
        
class Payment(models.Model):
    
    class Meta:
        ordering = ('-created',)
    
    account = models.ForeignKey(Account)
    subscription = models.ForeignKey("Subscription")
    created = models.DateTimeField("Processed", default=datetime.now, editable=False)
    amount = models.DecimalField(max_digits=11, decimal_places=2)
    response_message = models.TextField(blank=True)
    success = models.BooleanField()
    
    # Additional info that may or may not be used, depending on the gateway.
    trans_id = models.CharField("Transaction ID", blank=True, max_length=100)
    auth_code = models.CharField("Authorization", blank=True, max_length=100)
    order_number = models.CharField("Order number", blank=True, max_length=100)
    
    # CARD INFO: Keep a duplicate of the card info used.
    card_holder = models.CharField(max_length=100)
    card_type = models.CharField(max_length=50)
    card_end_digits = models.IntegerField(max_length=4)
    expiry = models.DateTimeField()
    
    def __unicode__(self):
        return '%s ($%s): %s' % (self.account, self.amount, self.result)
        
    def last_day_of_period(self):
        if self.success:
            return self.created + timedelta(days=settings.RENEWAL_PERIOD - 1)
        else:
            return self.created + timedelta(days=1)
            
    def generate_receipt(self):
        """
        Generates the email message for a Payment (both success and failure).
        Template should handle if-branching to support both cases.
        """
        from django.template import loader
        return loader.render_to_string('accounts/payment_user_receipt.txt', {
            'payment': self,
            'account': self.account,
            'success':self.success,
            'plan':self.subscription.plan })
        
    @property
    def result(self):
        if self.success:
            return "APPROVED"
        return "DECLINED"


class AccountUserProxy(object):
    """
    A mixin you can use in your AccountUser model to provide proxy access to the AccountUser attributes.
    For example: `class MyUserProfile(models.Model, AccountUserProxy):` will let you access MyUserProfile.account_user
    properties directly, ie: `myuser.account` instead of `myuser.account_user.account`
    """
    
    @property
    def number(self):
        return self.account_user.number
    
    @property
    def username(self):
        return self.account_user.username
        
    @property
    def user(self):
        return self.account_user.user
        
    @property
    def account(self):
        return self.account_user.account

    @property
    def created(self):
        return self.account_user.created

    @property
    def active(self):
        return self.account_user.active


class AccountUser(models.Model):
    number = models.CharField("User #", max_length=8)
    username = models.CharField(max_length=100)
    user = models.ForeignKey(User, editable=False, unique=True, related_name="account_membership")
    account = models.ForeignKey(Account, related_name="user_profiles")
    created = models.DateTimeField(default=datetime.now)
    active = models.BooleanField(default=True)
    
    class Meta:
        unique_together = (("username", "account"), ("number", "account"),)
        verbose_name = "user"
        verbose_name_plural = "users"
        
    def __unicode__(self):
        return u"%s [%s]" % (self.username, self.account.label)
    
    @property   
    def email(self):
        return self.user.email
    
    @property
    def is_active(self):
        return self.active
        
    def get_profile(self):
        """
        Works similar to contrib.auth.User.get_profile():
        Returns this User's profile associated to this specific Account.
        Allows application-specific profiles (ex: permissions, job titles, etc) to be plugged in,
        with an additional layer of indirection (ie: a User can have both a global UserProfile and a profile specific to each account).
        This will fail LOUDLY if called:
            - When `ACCOUNT_USER_PROFILE_MODULE` has NOT been defined in settings.py
            - When no model can be found (ie: MyAccountUserModel.DoesNotExist will be raised).
        """
        if not hasattr(self, '_profile_cache'):
            model = get_account_user_profile_model()
            self._profile_cache = model._default_manager.get(account_user__id=self.id)
            self._profile_cache.accountuser = self
        return self._profile_cache
        
    def save(self, *args, **kwargs):
        if not self.number:
            self.number = new_user_number()
        super(AccountUser, self).save(*args, **kwargs)
        self.user.save()

    def delete(self, *args, **kwargs):
        self.user.delete()
        super(AccountUser, self).delete()
        
    def send_welcome_email(self):
        email_subject = getattr(settings, 'ACCOUNT_WELCOME_EMAIL_SUBJECT', 'set settings.ACCOUNT_WELCOME_EMAIL_SUBJECT')
        
        message = render_to_string('accounts/welcome_email.txt',
            {'site':self.account.site,
            'account':self.account,
            'user':self})
        WelcomeEmailThread(email_subject,message,self.user.email).start()       


class RatePlan(models.Model):
    """
    A RatePlan is a product offering that allows you to build tiered services.
    TODO: Add renewal_period (1 month, 365 days, etc), and trial period?
    """
    objects = ActiveManager()
    name = models.CharField(max_length=100)
    position = models.IntegerField(default=0)
    setup_fee = models.IntegerField(blank=True, default=0)
    monthly_fee = models.IntegerField(blank=True, default=0)
    created = models.DateTimeField(default=datetime.now)
    modified = models.DateTimeField()
    active = models.BooleanField(default=True)
    
    class Meta:
        ordering = ('position', '-name')
    
    def __unicode__(self):
        return u"%s ($%s/month)" % (self.name, self.monthly_fee)
        
    @property
    def features(self):
        # This makes dictionary-lookups possible: plan.features['file_storage']
        # TODO: It would be nicer to use a dot-syntax: plan.features.file_storage
        class FeatureWrapper:
            def __init__(self, features):
                self.features = features
            def __getitem__(self, label):
                try:
                    return self.features.get(feature__label=label).value
                except:
                    return None
        return FeatureWrapper(self.rateplanfeature_set.all())
        
    def get_feature_value(self, feature):
        if isinstance(feature, Feature):
            feature = feature.label
        try:
            raw_value = self.rateplanfeature_set.get(feature__label=feature).value
            func_dict = getattr(settings, 'FEATURE_VALUES', {})
            if feature in func_dict:
                return func_dict[feature](raw_value)
            return raw_value
        except RatePlanFeature.DoesNotExist:
            return None
        
    def save(self, *args, **kwargs):
        self.modified = datetime.now()
        super (RatePlan, self).save(*args, **kwargs)


class Feature(models.Model):
    """
    A Feature is some limiting condition that can be used to build RatePlans.
    Example: "Number of users"
    """
    name = models.CharField(max_length=100)
    # TODO: Enforce alphanumeric
    label = models.CharField(max_length=100, unique=True)
    position = models.IntegerField(default=0)
    description = models.CharField(blank=True, max_length=500)
    
    class Meta:
        ordering = ('position', '-name',)
    
    def __unicode__(self):
        return self.name


class RatePlanFeature(models.Model):
    """
    This is what sets a limit on a RatePlan.
    For example, to create a user limit of "one" on the free plan, you
    would create a RatePlan feature such as:
    RatePlanFeature(plan=free, feature=num_users, value=1)
    """
    plan = models.ForeignKey(RatePlan)
    feature = models.ForeignKey(Feature)
    value = models.CharField(max_length=200)
    
    class Meta:
        unique_together = (('plan', 'feature'),)
    
    def __unicode__(self):
        return u"%s: %s" % (self.feature, self.value)


class Subscription(models.Model):
    """
    A Subscription represents the RatePlan an Account is currently or was previously on.
    Ordering by "-created" lets you assume the most recently created subscription is the current one.
    TODO: Have a way to override RatePlanFeature limits on a per-account basis?
    TODO: This where billing stuff should go (amount, next_bill_date, etc).
    """
    account = models.ForeignKey(Account, related_name="subscriptions")
    plan = models.ForeignKey(RatePlan)
    created = models.DateTimeField(default=datetime.now)
    modified = models.DateTimeField()
    
    class Meta:
        ordering = ('-created',)
        unique_together = (('account', 'created'),)
        get_latest_by = 'created'
    
    def __init__(self, *args, **kwargs):
        super(Subscription, self).__init__(*args, **kwargs)
        for rpf in self.plan.rateplanfeature_set.all():
            setattr(self, 'is_allowed_%s' % rpf.feature.label, curry(self.is_allowed, rpf.feature))
    
    def __unicode__(self):
        return u'%s (%s)' % (self.account, self.plan)
    
    def is_allowed(self, feature):
        if isinstance(feature, Feature):
            feature = feature.label
        return test_feature(self.account, feature, self.plan)

        
    def save(self, *args, **kwargs):
        self.modified = datetime.now()
        super (Subscription, self).save(*args, **kwargs)
    
        
def test_feature(account, feature, plan=None, qualify=False):
    """
    Checks if the Account qualifies for a RatePlanFeature according to the limits of a specific RatePlan.

    Feature should be a string representing the Feature's label.

    If `plan` isn't provided, the Account's current plan is used.
    This lets you check if an Account has exceeded a limit of their current plan (ex: "You cannot add anymore users.") or if they 
    are eligible to change to another plan(ex: "You can't change to that plan because you already have more users than it allows.")
    """
    if not hasattr(settings, 'ACCOUNT_LIMIT_CHECKER'):
        # Always return true if no limits are enforced.
        return True
    if plan is None:
        plan = account.plan
    value = plan.get_feature_value(feature)
    mod = __import__(settings.ACCOUNT_LIMIT_CHECKER, {}, {}, settings.ACCOUNT_LIMIT_CHECKER[settings.ACCOUNT_LIMIT_CHECKER.rfind('.')+1:])
    try:
        if qualify:
            return getattr(mod, 'qualifies_for_%s' % feature)(account, value)
        else:
            return getattr(mod, 'check_%s' % feature)(account, value)
    except AttributeError:
        # No function was defined -- assume they're allowed the feature.
        return True
